[toc]

# 1. 开始

介绍一款基于 uni-app 的跨端组件库——press-ui (opens new window),也可用于普通H5项目。API 与 vant 一致,可以看作是 uni-app 版本的 vant。

为什么不用uni-ui或者uview等组件库呢?有两个原因:

  1. 我们项目是从H5项目转成uni-app项目的,项目比较大,改动成本高。
  2. uni-uiuview这些库的API易用性较差,与vant相比差距大。

# 2. 介绍

# 2.1. 基础介绍

  1. 70+ 基础组件,覆盖移动端主流场景
  2. 支持基于uni-app的H5、微信小程序、QQ小程序
  3. 支持普通H5项目
  4. 零外部依赖,不依赖三方 npm 包
  5. 提供丰富的中英文文档和组件示例
  6. 支持主题定制,内置 600+ 个主题变量
  7. 支持国际化,内置 16+ 种语言包

文档地址:https://novlan1.github.io/press-ui/ (opens new window)

三端示例体验地址:

# 2.2. 解决痛点

press-ui主要解决了以下痛点:

  1. 可支持包含vant的H5项目平滑迁移至uni-app项目,只需要改下引用地址和组件名称。
  2. 丰富的组件类型,以及易用的API,让uni-app开发变得简单。
  3. 支持国际化、主题定制等,组件灵活性更强

同时,将项目中业务组件沉淀到press-ui中,有以下好处:

  • 增强可维护性,提升开发效率
    • 通过整理代码,合并属性,分离业务逻辑等,让组件变纯粹,增强可维护性,进而提升效率
  • 减少业务和组件的耦合,降低各自复杂度,并减少bug
  • 封装核心逻辑,控制变化
    • 不用担心外部合作人员改乱代码,以及解决冲突时的覆盖问题
  • UI问题定位简单
    • 三端代码同时发布,以及多种类型的示例,覆盖面全,容易发现ui问题,以及三端表现不一致问题
  • 可提升性能
    • 通过自定义队伍数等变量,定位性能瓶颈,并解决性能问题
  • 提高可复用性,可应用到其他项目
  • 技术沉淀,技术积累,不断打磨组件细节

# 2.3. 应用场景

press-ui可应用于uni-app项目,或者普通的H5项目,目前已应用在王者赛宝、HoK Club、赛宝pro等项目中。

# 3. 如何使用

# 3.1. 用于uni-app项目

  1. 安装npm包
npm i press-ui
  1. 在页面中正常引入并使用

比如 message-detail 组件:

<template>
  <PressMessageDetail />
</template>
<script>
import PressMessageDetail from 'press-ui/press-message-detail/press-message-detail.vue'

export default {
  components: {
    PressMessageDetail, 
  }
}
</script>
  1. 配置vue.config.js

注意,需要在vue.config.js中配置下 transpileDependencies

module.exports = {
  transpileDependencies: ['press-ui'],
}

# 3.2. 用于普通H5项目

press-ui 比普通的组件只是多了条件编译,所以加一个支持条件编译的loader就可以解决了,loader代码地址在这里 (opens new window)

loader使用方法如下:

  1. 安装 npm 包:
npm i uni-plugin-light -D
  1. vue.config.js 中添加如下设置:
const LOADER_IFDEF = 'uni-plugin-light/lib/loader/ifdef-loader';

module.export = {
  chainWebpack(config) {
    config.module
      .rule('ifdef-loader')
      // 根据项目实际配置文件类型
      .test(/press-ui.*(\.vue|\.ts|\.js|\.css|\.scss)$/)
      .use(LOADER_IFDEF)
      .loader(LOADER_IFDEF)
      .options({
        context: { H5: true },
        type: ['css', 'js', 'html'],
      })
      .end();
  }
}

# 4. 共建

press-ui 项目地址在这里 (opens new window),文档在这里 (opens new window)

# 4.1. 后续规划

press-ui后续规划包括:

  • 支持AndroidiOS
  • 优化构建流程和开发体验
  • 减小npm包大小
  • 对外开源

# 4.2. 共建

欢迎体验、试用、共建,可以加入群聊,二维码过期可以私聊加入。

# 5. 技术细节

# 5.1. 项目结构

press-ui 的项目结构如下:

- docs            # 文档地址
- plugin          # [demo]工程用到的插件
- script          # 脚本
- src
  - common        # [demo]公共内容
  - packages      # 对外发布的包内容
  - pages         # [demo]页面内容
  - static        # [demo]静态内容
  - App.vue       # [demo]应用入口
  - main.js
  - pages.json

组件库除了组件外,还有文档、示例、工程化配置等部分。为了维护起来方便,我将示例、文档、组件都放在一个文件夹里,所以一个标准的组件文件夹目录如下:

- press-button
  - demo.vue            # 组件示例
  - press-button.vue    # 组件
  - README.md           # 组件中文文档
  - README.en-US.md     # 组件英文文档

src/packages下就是由这些组件文件夹和一些公共文件构成。

上面的组织结构并不能直接用,还需要把README.md移动到docs中,把demo.vue移动到src/pages中。这里我写了脚本用来监听这些文件变动,发生变动后就把它们拷贝到需要的位置上,命令为npm run dispatch

# 5.2. 监听文件变化

监听用的是gulp,这里有个比较重要的属性delay,允许在执行任务之前等待许多更改,比如删除文件夹这种操作。delay只支持这种方式:

function watchPackages(cb) {
  gulp.watch('./src/packages/**/*', { delay: 200 }, cb);
}

不支持这样使用:

const { watch } = require('gulp');

const watcher = watch(['input/*.js']);

watcher.on('change', function(path, stats) {
  console.log(`File ${path} was changed`);
});

watcher.on('add', function(path, stats) {
  console.log(`File ${path} was added`);
});

watcher.on('unlink', function(path, stats) {
  console.log(`File ${path} was removed`);
});

watcher.close();

参考:https://www.gulpjs.com.cn/docs/api/watch/

# 5.3. 组件来源

press-ui的组件并不都是从零开始写的,而是来源于vant和项目自身沉淀的组件。

对于vant组件转化,实现方式如下:

  1. 转化小程序版本的vant,即vant-weapp
  2. wxml/wxss/js/json转为vue文件,即uni-app编译的逆操作
  3. API 替换为uni-app的通用类型
  4. 条件编译处理 API 差异部分

这样做的好处是,可以大大提升组件编写效率,且 API 保持与vant一致,从而方便用户由h5项目平滑迁移到uni-app项目。

对于项目中沉淀的组件,需与业务完全解耦后沉淀,并且需要具有一定的通用性或复杂度。为什么呢,这样可以让press-ui更稳定,减少变化,发挥press-ui的作用。

# 5.4. 组件质量

如何评价一个组件呢,可以从下面几个维度:

  • 通用性,好的组件一定是通用的,即满足多种场景需求,表现上看就是有很多propsslot,开发者想怎样用就怎样用
  • 兼容性,可在低端机、横竖屏、大小屏等不同场景,都有良好的展示、交互效果
  • 性能,可满足数据量大等场景

对于业务组件,通用性强的前提是,与业务充分解耦,如何做到呢?

  • 不能存在业务状态码,多重判断逻辑应该前置完成
  • 关注点分离,关注组件自身,而非业务
  • 一个衡量标准是业务更新迭代,业务组件却一直稳定
  • 能用函数调用的,就用函数调用,为什么呢?因为jshtml更灵活,灵活意味着通用型更强

# 5.5. BEM

关于BEM (opens new window)可以参考这篇文章 (opens new window)

笔者对项目中之前的组件,全部进行了BEM改造,以press-dialog为例,改造前:

改造后:

可以看出,可读性大幅增强。

# 5.6. 新组件一键接入

新增组件只需要执行命令:

npm run new:comp

然后交互式的输入组件英文名、中文名等内容即可。

为了维护方便,内部会有一个component-config.json文件,保存了所有的组件信息,包括名称、类型等。文档和示例的路由配置都是从这个文件生成。

这样的好处是要删除一个组件时,只要改变这个配置文件,然后执行npm run gen:config即可,同理,对于组件名称变动、类型变动也是一样的。

这个配置文件相当于一个收口,可以很方便的管理文档和示例。

function main() {
  writeDocSidebar();
  writeDemoIndexConfig();
  writeDemoPagesJson();
  writeDemoTitleI18n();
}

可以看出,这个配置主要驱动了:

  • 文档的sidebar
  • 示例的首页列表
  • 示例的pages.json
  • 示例的i18n配置

# 5.7. 类名兼容

press-ui的类名前缀统一为press-,而vant的类名前缀为van-,对于老项目,press-ui提供了一种兼容方案,允许不改动之前的样式文件。

使用方式为,给组件添加一个属性:extra-class-prefix="van-"

# 5.8. API平滑升级方案

组件API,可分为propseventevent的话直接多emit新的就好了,props的话需要先判断新旧prop的值是否为默认值。

如果哪一个不等则使用那一个,新prop优先级大于旧prop。如果都相等,就用新prop的值。

注意,这里的相等不能是简单的===,需要考虑引用类型。

此外,还要考虑取子属性的场景,比如this.a.b.c

export function getPropOrData({
  isFunctionMode,
  functionModeData,
  allProps,
  propsKeyMap = {},
  context,
  key,
}: {
  isFunctionMode: boolean;
  functionModeData: Record<string, any>;
  allProps: PropType;
  propsKeyMap: Record<string, string>;
  context: any;
  key: string;
}) {
  if (!isFunctionMode) {
    const oldKey = propsKeyMap[key];
    // 存在旧的key
    if (oldKey) {
      const oldDefaultValue = getDefaultValue(allProps, oldKey);
      const newDefaultValue = getDefaultValue(allProps, key);

      if (!isObjectEqual(context[key], newDefaultValue)) {
        return findObjectDeepValue(context, key);
      }
      if (!isObjectEqual(context[oldKey], oldDefaultValue)) {
        return findObjectDeepValue(context, oldKey);
      }
      return findObjectDeepValue(context, key);
    }
    return findObjectDeepValue(context, key);
  }
  return findObjectDeepValue(functionModeData, key);
}


function findObjectDeepValue(obj: Record<string, any>, key: string) {
  const list = key.split('.');
  let cur = obj;
  for (let i = 0; i < list.length;i++) {
    cur = cur[list[i]];
    if (!cur) return;
  }
  return cur;
}

# 5.9. children循环引用

循环引用的对象不要放在data/computed中,直接在created中声明,比如press-tabs中的children

# 5.10. 跨组件通信

vant-weapp用的是reletionpress-ui用的是provide/inject,也可以用eventBus